/*
 * Copyright (c) 2011-2012 Alexander Dubu
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * o  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * o  Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * o  Neither the name Axil nor the names of its contributors may be used to
 *    endorse or promote products derived from this software without specific
 *    prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package axil.tool.axili;

import axil.api.Axil;
import axil.api.AxilObject;
import axil.api.Script;
import axil.api.error.AxilException;
import axil.api.extend.Environment;
import axil.engine.AxilContext;
import axil.engine.Config;
import axil.stdlib.core.type.Nil;
import axil.tool.Tool;
import axil.tool.axili.command.*;
import axil.tool.axili.option.Configure;
import axil.tool.axili.option.Use;
import axil.tool.option.Option;
import axil.tool.option.Quiet;
import axil.tool.option.Summary;
import axil.tool.option.Version;

import java.io.*;
import java.util.*;
import java.util.Map.Entry;

import static axil.framework.Functions.*;


/**
 * The Axil interpreter, typically used only as a learning aid since Axil is
 * intended to be embedded ina host application. This is more of a demonstration
 * of capabilities than a full-blown interpreter.
 */
public class Axili extends Tool {
	private static final Map<String,Option> options = new HashMap<String, Option>();
	static {
		install(new Summary());
		install(new Version());
		install(new Quiet());
		install(new Use());
		install(new Configure());
	}
	private static void install(Option option) {
		options.put(option.keyword(), option);
		options.put(option.shortcut(), option);
	}

	private static final Map<String,Command> commands = new HashMap<String, Command>();
	private static final List<Command> list = new ArrayList<Command>();
	static {
		install(new Help());
		install(new Describe());
		install(new Remember());
		install(new Forget());
		install(new Bindings());
		install(new Functions());
		install(new Internals());
		install(new Exit());
		install(new Quit());
		Collections.sort(list);
	}
	private static void install(Command cmd) {
		commands.put(cmd.keyword(), cmd);
		commands.put(cmd.shortcut(), cmd);
		list.add(cmd);
	}
	private static final String[] NONE = new String[] {""};

	private String setup;
	private List<String> modules;
	private Axil axil;
	private Config config;
	private String script;
	private AxilContext context;
	private LineNumberReader input;
	private Writer output;
	private boolean internals;
	private AxilObject last;


	public Axili(Map<String, Option> options) {
		super("Axil Interpreter", options);
		this.modules = new ArrayList<String>();
		this.axil = Axil.instance();
		this.input = new LineNumberReader(new InputStreamReader(System.in));
		this.output = new PrintWriter(System.out);
		this.last = Nil.object;
		internals = false;
	}


	/**
	 * Add the given string as a common line argument (not a '-' option) for
	 * this tool. This method must check the validity of the argument.
	 */
	protected void arg(String arg) {
		if (! arg.endsWith(".axil")) {
			abort("Expecting an Axil script, found " + quote(arg) + " instead.");
		}
		if (script != null) {
			this.abort("You cannot specify more than one script for execution.");
		}
		script = arg;
	}


	/**
	 * Execute this tool.
	 */
	protected void execute() {
		configure();
		if (script == null) {
			interpreter();
		} else {
			run(script);
		}
		Properties p = System.getProperties();
		for (Entry e : p.entrySet()) {
			System.out.println(e.getKey().toString() + " = " + e.getValue().toString());
		}
	}


	private void configure() {
		if (setup == null) {
			String os = System.getProperty("os.name");
			if (os.equals("Max OS X")) {
				setup = System.getProperty("user.home") +
				        "/Library/Application Support/Axil/axil.properties";
			} else {
				if (os.equals("Windows XP")) {
					setup = "C:\\Documents and Settings\\" +
					        System.getProperty("user.name") +
					        "\\Application Data\\Axil\\axil.properties";
				} else {
					if (os.equals("Windows 7") || os.equals("Windows Vista")) {
						setup = "C:\\Users\\" + System.getProperty("user.name") +
						        "\\AppData\\Local\\\\Axil\\axil.properties";
					} else {
						setup = System.getProperty("user.home") + "/.axilrc";
					}
				}
			}
		}
		File file = new File(setup);
		if (file.exists()) {
			config = new Config(file);
		} else {
			config = (Config)Axil.instance().configuration();
		}
		for (String module : modules) {
			config.use(module);
		}
	}


	private void run(String filename) {
		Reader r = null;
		try {
			r = new FileReader(filename);
			Script s = axil.compile(config, context, filename, r);
			Environment env = axil.environment(config, context, persona(), output);
			s.eval(env);
		} catch (FileNotFoundException e) {
			throw new RuntimeException(e);
		} finally {
			close(r);
		}
	}


	private void interpreter() {
		context = AxilContext.empty;
		while (true) {
			String line = read();
			if (! empty(line)) {
				String[] parts = NONE;
				Command cmd = commands.get(line);
				if (cmd == null) {
					parts = line.split(" ");
					if (parts.length > 1) {
						if (Character.isLetter(parts[1].charAt(0))) {
							cmd = commands.get(parts[0]);
						}
					}
				}

				if (cmd != null) {
					if (cmd.args() != (parts.length - 1)) {
						print("Incorrect number of arguments for the " +
						      quote(cmd.keyword()) + " command.");
					} else {
						cmd.execute(this, parts);
					}
				} else {
					try {
						Script s = axil.compile(config, context, line);
						Environment env = axil.environment(config, context,
						                                   persona(), output);
						AxilObject r = s.eval(env);
						print(r.toString() + " (" + r.type() + ')');
						last = r;
					} catch (AxilException e) {
						print(e);
						if (internals) {
							e.printStackTrace();
						}
					}
				}
			}
		}
	}


	private String read() {
		System.out.print("Axil> ");
		System.out.flush();
		String line = null;
		try {
			line = input.readLine();
		} catch (IOException e) {
			// IGNORED
		}
		if (line == null) {
			line = "quit";
		}
		return line.trim();
	}


	public void setup(String file)				{
		this.setup = file;
	}
	public Config config() 						{
		return config;
	}
	public Collection<Command> commands()		{
		return list;
	}
	public void module(String module)			{
		modules.add(module);
	}
	public AxilContext context()				{
		return context;
	}
	public void context(AxilContext context) 	{
		this.context = context;
	}
	public AxilObject last()					{
		return last;
	}
	public void internals(boolean v)			{
		internals = v;
	}


	public static void main(String[] args) {
		new Axili(options).run(args);
	}
}
